/**
 * SLF4jLog
 *
 * @author Chris Pratt
 *
 * 10/23/2007
 */
package com.anodyzed.onyx.log.logback;

import ch.qos.logback.classic.Level;

import com.anodyzed.onyx.bean.BeanUtils;
import com.anodyzed.onyx.cache.Cache;
import com.anodyzed.onyx.cache.CacheLoader;
import com.anodyzed.onyx.cache.MRUCache;
import com.anodyzed.onyx.cache.SimpleCache;
import com.anodyzed.onyx.log.Log;
import com.anodyzed.onyx.log.LogLevel;
import com.anodyzed.onyx.text.TextFormat;
import com.anodyzed.onyx.util.SplayTreeMap;

import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.spi.LocationAwareLogger;

/**
 * Java 5+ Logback Facade - Allows using format strings and variable argument 
 * lists to build log messages efficiently. 
 *
 * Since there is no up-front string processing used to construct the log
 * message, if the logger is set above the level of the log method called, it is
 * quickly and quietly ignored, with no processing.
 *
 * If, on the other hand, the log level is below the level of the log method
 * called, the format string is processed and the supplied arguments are used to
 * build the log message.
 *
 * The Log message format string is comprised of static text with interspersed
 * replacement argument placeholders.  The placeholders begin with '{' and end
 * with '}'.
 *
 * Example:
 *   <code>"Simple {0}, More Complex {1.lastname}, Even More Complex
 *   {2.address.city}, Most Complex {1.birthDate,date,yyyyMMdd}"</code>
 *
 * The simplest is just the (zero based) index of the argument used to replace
 * the placeholder.  Additionally you can add a dot separated string of bean
 * property names to access from the indexed argument. Lastly, you can use the
 * java.text.MessageFormat style comma separated format descriptors to format
 * each replacement element.
 *
 * Example:
 *   <code>log.debug("Simple {0}, More Complex {1.lastname}, Even More Complex
 *   {2.address.city}, Most Complex {1.birthDate,date,yyyyMMdd}",
 *   5, user, company);</code>
 *
 * Might write out:
 *   <code>Simple 5, More Complex Chris, Even More Complex Sacramento,
 *   Most Complex 19640123</code>
 *
 * Oh, one last thing, if the last argument is an instance of Throwable, it will
 * be passed on to Logback's Throwable support 
 *  
 * @see com.anodyzed.onyx.text.TextFormat 
 */
public class LogbackLog implements Log {
  private static final String FQCN = LogbackLog.class.getName();
  private static final TextFormat BEAN_PROPERTY = new TextFormat("  {0}: {1}");

  private static final Cache<String,TextFormat> formatCache = new MRUCache<>(new SimpleCache<>("formatCache",null,new CacheLoader<String,TextFormat>() {
    @Override
    public TextFormat load (Object arg,String key) {
      return new TextFormat(key);
    } //load
  },new SplayTreeMap<String,TextFormat>()),100);

  private Logger log;

  /**
   * Constructor
   *
   * @param log The Logger to Delegate to
   */
  protected LogbackLog (Logger log) {
    this.log = log;
  } //Log

  /**
   * Get the Wrapped Logger
   *
   * @return Wrapped Logger instance
   */
  public Logger getWrappedLogger () {
    return log;
  } //getWrappedLogger

  /**
   * Convert the Logback Level integers to the Location Aware Logger Level 
   * integers 
   *
   * @param original Logging Level
   * @return Adjusted Logging Level
   */
//  private int toLevel (int lvl) {
//    if(lvl > 100) {
//      switch(lvl) {
//        case Level.TRACE_INT:
//          return LocationAwareLogger.TRACE_INT;
//        case Level.DEBUG_INT:
//          return LocationAwareLogger.DEBUG_INT;
//        case Level.INFO_INT:
//          return LocationAwareLogger.INFO_INT;
//        case Level.WARN_INT:
//          return LocationAwareLogger.WARN_INT;
//        case Level.ERROR_INT:
//          return LocationAwareLogger.ERROR_INT;
//      }
//    }
//    return lvl;
//  } //toLevel

  /**
   * Convert the Log Level to a Logback Level
   *
   * @param level The Log Level
   * @return The Logback Level
   */
  private Level toLevel (LogLevel level) {
    switch(level) {
      case All:
        return Level.ALL;
      case Trace:
        return Level.TRACE;
      case Debug:
        return Level.DEBUG;
      case Info:
        return Level.INFO;
      case Warn:
        return Level.WARN;
      case Error:
        return Level.ERROR;
      case None:
        return Level.OFF;
    }
    return null;
  } //toLevel

  /**
   * Write the formatted message (and possible Throwable) to the log
   *  
   * @param fqcn The Fully Qualified Class Name of the Log class being used 
   * @param level The Message Priority integer
   * @param fmt The <code>TextFormat</code>
   * @param args Replacement Arguments 
   */
  public void write (String fqcn,int level,TextFormat fmt,Object... args) {
    try {
      String msg = fmt.format(args);
      Throwable t = ((args != null) && (args.length > 0) && (args[args.length - 1] instanceof Throwable)) ? (Throwable)args[args.length - 1] : null;
      ((ch.qos.logback.classic.Logger)log).log(null,fqcn,level,msg,null,t);
    } catch(IndexOutOfBoundsException x) {
      ((ch.qos.logback.classic.Logger)log).log(null,fqcn,LocationAwareLogger.ERROR_INT,"Invalid Index in Log Statement \"{}\" - {}",new Object[] {fmt,x.getMessage()},null);
    }
  } //write

  /**
   * Test whether the logger is enabled for the supplied Logback Priority
   *
   * @param level The Logging Priority Level
   * @return true if enabled
   */
  @Override
  public boolean isEnabledFor (LogLevel level) {
    return ((ch.qos.logback.classic.Logger)log).isEnabledFor(toLevel(level));
  } //isEnabledFor

  /**
   * Write a Log Message at the supplied Logback Priority
   *
   * @param level The Logging Priority Level
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void log (LogLevel level,String fmt,Object... args) {
    if(isEnabledFor(level)) {
      write(FQCN,toLevel(level).toInt(),formatCache.get(fmt),args);
    }
  } //log

  /**
   * Write a Log Message at the supplied Logback Priority
   *
   * @param level The Logging Priority Level
   * @param fmt The Log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void log (LogLevel level,TextFormat fmt,Object... args) {
    if(isEnabledFor(level)) {
      write(FQCN,toLevel(level).toInt(),fmt,args);
    }
  } //log

  /**
   * Log all the Readable Properties in a Bean
   *
   * @param level The Logging Priority Level integer
   * @param msg The lead in Message 
   * @param bean The Bean to Log 
   */
  @Override
  public void bean (LogLevel level,String msg,Object bean) {
    Level lvl = toLevel(level);
    if(((ch.qos.logback.classic.Logger)log).isEnabledFor(toLevel(level))) {
      int ilvl = lvl.toInt();
      if(msg != null) {
        ((LocationAwareLogger)log).log(null,FQCN,ilvl,formatCache.get(msg).format(bean),null,null);
      }
      if(bean != null) {
        try {
          for(Method method : BeanUtils.getAccessors(bean.getClass())) {
            try {
              ((LocationAwareLogger)log).log(null,FQCN,ilvl,BEAN_PROPERTY.format((Object)method.getName(),method.invoke(bean)),null,null);
            } catch(IllegalAccessException | InvocationTargetException ignore) {
              // fallthru
            }
          }
        } catch(IntrospectionException ignore) {
        }
      } else {
        ((LocationAwareLogger)log).log(null,FQCN,ilvl,"<null>",null,null);
      }
    }
  } //bean

  /**
   * Check whether Trace Logging is Enabled
   *
   * @return true if trace logging is enabled
   */
  @Override
  public boolean isTraceEnabled () {
    return log.isTraceEnabled();
  } //isTraceEnabled

  /**
   * Write out a trace level message
   *
   * @param fmt The Log Message Format String
   * @param args The replacement arguments
   */
  @Override
  public void trace (String fmt,Object... args) {
    if(log.isTraceEnabled()) {
      write(FQCN,LocationAwareLogger.TRACE_INT,formatCache.get(fmt),args);
    }
  } //trace

  /**
   * Write out a trace level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void trace (TextFormat fmt,Object... args) {
    if(log.isTraceEnabled()) {
      write(FQCN,LocationAwareLogger.TRACE_INT,fmt,args);
    }
  } //trace

  /**
   * Write out the bean properties at the trace level
   *
   * @param msg A descriptive message
   * @param bean The Bean being debugged
   */
  @Override
  public void traceBean (String msg,Object bean) {
    if(log.isTraceEnabled()) {
      bean(LogLevel.Trace,msg,bean);
    }
  } //traceBean

  /**
   * Check whether Debug Logging is enabled
   *
   * @return true if debug logging is enabled
   */
  @Override
  public boolean isDebugEnabled () {
    return log.isDebugEnabled();
  } //isDebugEnabled

  /**
   * Write out a debug level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void debug (String fmt,Object... args) {
    if(log.isDebugEnabled()) {
      write(FQCN,LocationAwareLogger.DEBUG_INT,formatCache.get(fmt),args);
    }
  } //debug

  /**
   * Write out a debug level message
   *
   * @param fmt The Log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void debug (TextFormat fmt,Object... args) {
    if(log.isDebugEnabled()) {
      write(FQCN,LocationAwareLogger.DEBUG_INT,fmt,args);
    }
  } //debug

  /**
   * Write out the bean properties at the debug level
   *
   * @param msg A descriptive message
   * @param bean The Bean being debugged
   */
  @Override
  public void debugBean (String msg,Object bean) {
    if(log.isDebugEnabled()) {
      bean(LogLevel.Debug,msg,bean);
    }
  } //debugBean

  /**
   * Check whether informational logging is enabled
   *
   * @return true if enabled
   */
  @Override
  public boolean isInfoEnabled () {
    return log.isInfoEnabled();
  } //isInfoEnabled

  /**
   * Write out an informational level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void info (String fmt,Object... args) {
    if(log.isInfoEnabled()) {
      write(FQCN,LocationAwareLogger.INFO_INT,formatCache.get(fmt),args);
    }
  } //info

  /**
   * Write out an informational level message
   *
   * @param fmt The Log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void info (TextFormat fmt,Object... args) {
    if(log.isInfoEnabled()) {
      write(FQCN,LocationAwareLogger.INFO_INT,fmt,args);
    }
  } //info

  /**
   * Check whether warning logging is enabled
   *
   * @return true if enabled
   */
  @Override
  public boolean isWarnEnabled () {
    return log.isWarnEnabled();
  } //isWarnEnabled

  /**
   * Write out a warning level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void warn (String fmt,Object... args) {
    if(log.isWarnEnabled()) {
      write(FQCN,LocationAwareLogger.WARN_INT,formatCache.get(fmt),args);
    }
  } //warn

  /**
   * Write out a warning level message
   *
   * @param fmt The Log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void warn (TextFormat fmt,Object... args) {
    if(log.isWarnEnabled()) {
      write(FQCN,LocationAwareLogger.WARN_INT,fmt,args);
    }
  } //warn

  /**
   * Write out an error level message
   *
   * @param fmt The log message format string
   * @param args The replacement arguments
   */
  @Override
  public void error (String fmt,Object... args) {
    if(log.isErrorEnabled()) {
      write(FQCN,LocationAwareLogger.ERROR_INT,formatCache.get(fmt),args);
    }
  } //error

  /**
   * Write out an error level message
   *
   * @param fmt The log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void error (TextFormat fmt,Object... args) {
    if(log.isErrorEnabled()) {
      write(FQCN,LocationAwareLogger.ERROR_INT,fmt,args);
    }
  } //error

  /**
   * Get the Effective Level for this Logger
   *
   * @return Logging Level
   */
  @Override
  public LogLevel getEffectiveLevel () {
    switch(((ch.qos.logback.classic.Logger)log).getEffectiveLevel().toInt()) {
      case Level.DEBUG_INT:
        return LogLevel.Debug;
      case Level.TRACE_INT:
        return LogLevel.Trace;
      case Level.INFO_INT:
        return LogLevel.Info;
      case Level.WARN_INT:
        return LogLevel.Warn;
      case Level.ERROR_INT:
        return LogLevel.Error;
      case Level.ALL_INT:
        return LogLevel.All;
      case Level.OFF_INT:
        return LogLevel.None;
    }
    return null;
  } //getEffectiveLevel

} //*SLF4jLog
